昨天寫 Helmet 的時候順便提到了 CORS。就是這個在開發時讓人頭痛又不陌生的東西「Access to XMLHttpRequest has been blocked by CORS policy」,今天就來整理 CORS 到底是什麼。
CORS 全名是 Cross-Origin Resource Sharing(跨來源資源共享)。聽起來很專業,但其實概念很簡單:就是讓瀏覽器知道「這個網站可以跟那個網站交換資料」。
為什麼需要這個機制?因為瀏覽器有一個叫做「同源政策」(Same-Origin Policy)的安全機制。
同源政策是瀏覽器最基本的安全功能之一。它規定:一個網頁的 JavaScript 只能存取「同源」的資源。
什麼叫「同源」?必須同時滿足三個條件:
來看幾個例子:
原始網址:https://example.com:443/page
同源:
✓ https://example.com:443/api/users
✓ https://example.com:443/about
不同源:
✗ http://example.com:443/page (協定不同)
✗ https://api.example.com:443/page (網域不同)
✗ https://example.com:8080/page (埠號不同)
假設沒有同源政策,你登入了銀行網站,然後又打開了一個惡意網站。這個惡意網站的 JavaScript 就可以直接存取銀行網站的 cookie,然後用你的身分轉帳。
有了同源政策,惡意網站的 JavaScript 就無法存取銀行網站的資料,因為它們不同源。
現代網頁開發常常是前後端分離:
http://localhost:3000
http://localhost:8080
這兩個網址明顯不同源(埠號不同),所以前端的請求會被瀏覽器擋下來。這時候就需要 CORS 了。
CORS 就是一套機制,讓後端可以告訴瀏覽器:「我允許某些不同源的網站來存取我的資源」。
CORS 主要透過 HTTP 標頭來溝通。最重要的幾個標頭是:
這是最關鍵的標頭,告訴瀏覽器哪些來源可以存取資源。
// 允許所有來源(不安全,別在正式環境這樣用)
Access-Control-Allow-Origin: *
// 只允許特定來源
Access-Control-Allow-Origin: https://example.com
指定允許的 HTTP 方法。
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
指定允許的請求標頭。
Access-Control-Allow-Headers: Content-Type, Authorization
是否允許發送 cookies。
Access-Control-Allow-Credentials: true
這是 CORS 中比較特別的部分。對於某些「複雜」的請求,瀏覽器會先發送一個 OPTIONS 請求,問後端「我可以發這個請求嗎?」
什麼樣的請求會觸發 preflight?
application/x-www-form-urlencoded
multipart/form-data
text/plain
Authorization
)流程是這樣的:
1. 瀏覽器:「我想發一個 POST 請求,帶有 Authorization header,可以嗎?」
(發送 OPTIONS 請求)
2. 後端:「可以啦,哪次不可以」
(回傳 CORS 標頭)
3. 瀏覽器:「好,已發」
(發送實際的 POST 請求)
最簡單的方法是用 cors
這個套件:
npm install cors
const express = require('express');
const cors = require('cors');
const app = express();
// 允許所有來源(開發環境可以,正式環境別這樣)
app.use(cors());
app.get('/api/data', (req, res) => {
res.json({ message: 'Hello CORS!' });
});
app.listen(8080);
// 只允許特定網域
app.use(cors({
origin: 'https://example.com'
}));
// 允許多個網域
app.use(cors({
origin: ['https://example.com', 'https://app.example.com']
}));
// 動態決定
const allowedOrigins = ['https://example.com', 'https://app.example.com'];
app.use(cors({
origin: function (origin, callback) {
// 允許沒有 origin 的請求(像 Postman、curl)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
}));
app.use(cors({
origin: 'https://example.com',
credentials: true // 允許發送 cookies
}));
注意:如果 credentials: true
,就不能用 origin: '*'
,必須明確指定來源。
// 只有特定路由需要 CORS
app.get('/api/public', cors(), (req, res) => {
res.json({ message: 'Public API' });
});
// 其他路由不套用 CORS
app.get('/api/internal', (req, res) => {
res.json({ message: 'Internal API' });
});
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [
'https://example.com',
'https://app.example.com'
];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400 // preflight 結果快取 24 小時
};
app.use(cors(corsOptions));
檢查清單:
*
可能是其他中介軟體在 cors 之前處理了請求。確保 cors 在最前面:
app.use(cors());
app.use(express.json());
// ... 其他中介軟體
檢查環境變數設定:
const allowedOrigins = process.env.NODE_ENV === 'production'
? ['https://example.com']
: ['http://localhost:3000', 'http://localhost:3001'];
app.use(cors({
origin: allowedOrigins
}));
前端也要設定:
// fetch
fetch('https://api.example.com/data', {
credentials: 'include'
});
// axios
axios.get('https://api.example.com/data', {
withCredentials: true
});
後端:
app.use(cors({
origin: 'https://example.com',
credentials: true
}));
如果想更細緻的控制,也可以手動設定:
app.use((req, res, next) => {
const allowedOrigins = ['https://example.com'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 處理 preflight
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
在前端專案中設定代理,就不用處理 CORS 了:
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
}
// React (package.json)
{
"proxy": "http://localhost:8080"
}
開發時可以用 CORS 相關的 Chrome 擴充套件暫時關閉 CORS 檢查。但記得:
除非你的 API 真的是完全公開的,不然不要這樣設定。這等於讓所有網站都可以存取你的 API。
不要用正規表達式去比對 origin,容易出錯。用陣列明確列出允許的網域。
// 不好的做法
origin: /\.example\.com$/
// 好的做法
origin: [
'https://www.example.com',
'https://app.example.com',
'https://admin.example.com'
]
https://example.com
和 https://sub.example.com
是不同源的。如果需要允許所有子網域,要明確列出或用函式判斷。
如果設定了 credentials: true
,一定要確保 origin 的白名單是正確的。不然其他網站可以帶著使用者的 cookie 存取你的 API。